package br.unifor.g2cl;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Enumeration;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import net.sf.jgcs.ControlSession;
import net.sf.jgcs.DataSession;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;

import br.unifor.g2cl.annotation.PostConstruct;

/**
 * Class used to create a {@link IMarshalDataSession} stack, parsing a string or a XML file.
 * If no stack is specified, only a {@link MarshalDataSession} is created (MarshalDataSession is always the bottom of the stack).
 *  
 * 
 * @author Leandro Sales (leandro.shp@gmail.com)
 * @author Henrique Teofilo (henriquetft@gmail.com)
 */
public class MarshalDataSessionFactory {

	/** String or file to parse */
	private String stack;

	private static Logger logger = Logger
			.getLogger(MarshalDataSessionFactory.class.getName());

	public MarshalDataSessionFactory(String stack) {
		this.stack = stack;
	}

	public MarshalDataSessionFactory() {
		this(null);
	}

	public IMarshalDataSession openMarshalDataSession(ControlSession control,
			DataSession data) throws Exception {

		logger.config("MarshalDataSession stack string: " + stack);

		IMarshalDataSession session = new MarshalDataSession(data);

		if (stack == null) {
			logger.config("No string or file to configure stack. Using the default DataSession");
			return session;
		}
		
		if(stack.contains("classpath:")){
			//try to find in the classpath
			stack = stack.replace("classpath:", "");
			URL xmlStackURL = Thread.currentThread().getContextClassLoader().getResource(stack);

			if(xmlStackURL == null){
				throw new FileNotFoundException("File not found in the classpath!");
			}
			File xmlStack = new File(xmlStackURL.getFile());
			return createFromXML(xmlStack, control, session);
		}

		File file = new File(this.stack);
		if (file.isFile()) {
			logger.config("Stack string is a file. Parsing XML...");
			return createFromXML(file, control, session);
		} else {
			logger.config("Stack string is not a file. Parsing string...");
			return createFromString(this.stack, control, session);
		}
	}


	 /* ********************************************************************* *
	  * ********************** String configuration ************************* *
	  * ********************************************************************* */

	@SuppressWarnings("unchecked")
	private static IMarshalDataSession createFromString(String stack,
			ControlSession control, IMarshalDataSession session) throws ConstructException {

		/* Regex to match the pattern:
		 * class1(param1=value,param2=value)+class2(param1=value,param2=value)
		 * group 1 = class, group 2 = parameters
		 */
		Pattern p = Pattern.compile("(.+?)(\\(.+\\))?");
		
		// get the classes separated by +
		String[] decoratorClasses = stack.split("\\+");
		
		for (String l : decoratorClasses) {
			Matcher m = p.matcher(l);
			if (m.matches()) {
				// extracting className and parameters
				String className = m.group(1);
				String parameters = m.group(2);
				String properties = null;
				if (parameters != null) {
					// remove () from (parameters)
					properties = parameters.substring(1, parameters.length() - 1);
				}

				try {
					/* creates instance of the decorator and invikes set methods
					 * to set parameters on decorator object */
					Class clazz = Class.forName(className);
					Constructor c = null;
					c = clazz.getConstructor(IMarshalDataSession.class,
							ControlSession.class);
					Properties props = parseStringOptions(properties);
					session = (IMarshalDataSession) c.newInstance(session, control);

					Enumeration propertyNames = props.propertyNames();

					while (propertyNames.hasMoreElements()) {
						String property = (String) propertyNames.nextElement();
						Method method = getSetMethodFromAttributeName(property,
								clazz);
						method.invoke(session, props.getProperty(property));
					}
				} catch (Exception e) {
					logger.log(Level.SEVERE, "Error constructing stack", e);
					throw new ConstructException(e);
				}


			} else {
				ConstructException ex = new ConstructException("error " +
						"parsing string: " + stack + ". Incorrect format");
				logger.log(Level.SEVERE, "", ex);
				throw ex;
			}
		}

		return session;
	}

	private static Properties parseStringOptions(String properties)
			throws IOException {
		Properties props = new Properties();
		if (properties != null) {
			properties = properties.replace(",", "\n");
			ByteArrayInputStream bais = new ByteArrayInputStream(properties
					.getBytes());
			props.load(bais);

		}
		return props;
	}


	 /* ******************************************************************** *
	  * ************************ XML configuration ************************* *
	  * ******************************************************************** */

	/**
	 * 
	 * <g2cl:stack>
	 * 	<g2cl:decorator class="BundleDataSession" maxSize="10" timeout="1000"/>
	 * 		<g2cl:decorator class="CompressDataSession">
	 * 		<g2cl:worker class="Worker" attr=""/> 
	 *  </g2cl:decorator>
	 * </g2cl:stack>
	 * 
	 */
	@SuppressWarnings("unchecked")
	public static IMarshalDataSession createFromXML(File xmlFile,
			ControlSession control, IMarshalDataSession session) throws ConstructException {

		try {
			// fazer o parse do arquivo e criar o documento XML
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			DocumentBuilder db = dbf.newDocumentBuilder();
			Document doc = db.parse(xmlFile);

			// Gettin root node, must be "stack"
			Element stack = doc.getDocumentElement();

			if (stack.getNodeName().equals("stack")) {
				NodeList decorators = stack.getElementsByTagName("decorator");
				for (int i = 0; i < decorators.getLength(); i++) {
					Element decorator = (Element) decorators.item(i);
					String className = decorator.getAttribute("class");
					Class clazz = Class.forName(className);
					Constructor c = null;
					c = clazz.getConstructor(IMarshalDataSession.class,
							ControlSession.class);
					session = (IMarshalDataSession) c.newInstance(session, control);

					NamedNodeMap attrs = decorator.getAttributes();
					for (int j = 0; j < attrs.getLength(); j++) {
						String attrName = attrs.item(j).getNodeName();
						if (!attrName.matches("class")) {
							Method method = getSetMethodFromAttributeName(attrName,
									session.getClass());
							String value = attrs.item(j).getNodeValue();
							method.invoke(session, value);
						}
					}
					NodeList workerNode = decorator.getElementsByTagName("worker");
					if (workerNode.getLength() > 0) {
						Element workerElement = (Element) workerNode.item(0);
						className = workerElement.getAttribute("class");
						Class workerClass = Class.forName(className);
						Constructor<DataSessionWorker> constructorWorker = workerClass
								.getConstructor();
						DataSessionWorker worker = constructorWorker.newInstance();

						Method method = session.getClass().getMethod("setWorker",
								DataSessionWorker.class);
						method.invoke(session, worker);

						attrs = workerElement.getAttributes();
						for (int j = 0; j < attrs.getLength(); j++) {
							String attrName = attrs.item(j).getNodeName();
							if (!attrName.matches("class")) {
								method = getSetMethodFromAttributeName(attrName,
										worker.getClass());
								String value = attrs.item(j).getNodeValue();
								method.invoke(worker, value);
							}
						}
					}

					Method[] methods = session.getClass().getMethods();

					for (Method m : methods) {
						if (m.isAnnotationPresent(PostConstruct.class)) {
							m.invoke(session);
						}
					}

				}
			}
		} catch (Exception e) {
			logger.log(Level.SEVERE, "Error constructing stack", e);
			throw new ConstructException(e);
		}

		return session;
	}

	@SuppressWarnings("unchecked")
	private static Method getSetMethodFromAttributeName(String attribute,
			Class clazz) throws Exception {
		
		String firstLetter = attribute.substring(0, 1);
		String methodName = attribute.replaceFirst(firstLetter, "set"
				+ firstLetter.toUpperCase());
		return clazz.getMethod(methodName, String.class);
	}

}
